/*
 * @(#)XlsEngine.java
 */
package com.stony.poie.util;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;

import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.Region;

import com.stony.poie.basic.SimpleTRow;
import com.stony.poie.basic.XlsCombo;
import com.stony.poie.basic.XlsFile;
import com.stony.poie.basic.XlsTData;
import com.stony.poie.basic.XlsTRow;
import com.stony.poie.basic.XlsUnit;
import com.stony.poie.exception.IllegalContentException;
import com.stony.poie.exception.IllegalStyleException;
import com.stony.poie.ext.XlsFont;
import com.stony.poie.ext.XlsStyle;

/**
 * A XlsEngine can export data to and (or) import data from excel file.
 * <p>
 * Last modified: February 15, 2006
 * 
 * @author Thomas Wu
 * @version 1.0.2
 */
public final class XlsEngine {
	/** Deal with western words. */
	public final static short COMPRESSED_UNICODE = HSSFCell.ENCODING_COMPRESSED_UNICODE;

	/** Deal with Chinese words etc. */
	public final static short ENCODING_UTF_16 = HSSFCell.ENCODING_UTF_16;

	/* Wheather group rows or not. */
	private boolean groupRows;

	/* Wheather group columns or not. */
	private boolean groupCols;

	/* Y index. */
	private int y;

	/* X index. */
	private short x;

	/* XlsFile instance to be export. */
	private XlsFile xlsFile;

	/* Jakarta POI library related. */
	private HSSFWorkbook workbook;

	/* Jakarta POI library related. */
	private HSSFSheet sheet;

	/* Jakarta POI library related. */
	private HSSFRow row;

	/* Jakarta POI library related. */
	private HSSFCell cell;

	/**
	 * Construct a new XlsEngine instance with specified encoding.
	 * 
	 * @param encoding
	 *            encoding of excel file(every cell)
	 */
	public XlsEngine() {
		this.groupRows = false;
		this.groupCols = false;
	}

	/**
	 * Export excel file to the specified OutputStream.
	 * <p>
	 * 
	 * @param file
	 *            XlsFile instance to be exported.
	 * @param out
	 *            the output stream
	 * 
	 * @exception IllegalContentException
	 * @exception IllegalStyleException
	 * @exception IOException
	 */
	public void exportXlsFile(XlsFile file, OutputStream out)
			throws IllegalContentException, IllegalStyleException, IOException {
		workbook = new HSSFWorkbook();
		xlsFile = file;
		/* Generate styles. */
		createStyles();
		/* Iterate sheets. */
		for (int i = 0; i < file.getTables().length; i++) {
			String name = file.getTables()[i].getName();
			if (name == null || name.trim().equals("")) {
				sheet = workbook.createSheet();
			} else {
				sheet = workbook.createSheet(name);
				workbook.setSheetName(i, name);
			}
			y = x = 0;
			/* Create sheet. */
			exportXlsFileRecursion(file.getTables()[i], XlsTRow.class);
		}
		/* Write to OutputStream. */
		workbook.write(out);
		out.flush();
		out.close();
	}

	/**
	 * Overloaded method. Export excel file with specified file name.
	 * 
	 * @param file
	 *            XlsFile instance to be exported
	 * @param absoluteFileName
	 *            absolute file name
	 * 
	 * @exception IllegalContentException
	 * @exception IllegalStyleException
	 * @exception IOException
	 */
	public void exportXlsFile(XlsFile file, String absoluteFileName)
			throws IllegalContentException, IllegalStyleException, IOException {
		exportXlsFile(file, new FileOutputStream(absoluteFileName));
	}

	private void createStyles() {
		XlsStyle xlsStyle = null;
		HSSFCellStyle hssfStyle = workbook.createCellStyle();
		for (int i = 0; i < XlsFile.TOTAL_STYLES_NUM; i++) {
			xlsStyle = xlsFile.getStyle(i);
			hssfStyle = workbook.createCellStyle();
			hssfStyle.setAlignment(xlsStyle.getAlignment());
			hssfStyle.setVerticalAlignment(xlsStyle.getValignment());
			hssfStyle.setWrapText(xlsStyle.isWraptext());
			if (xlsStyle.getFormat() != null) {
				hssfStyle.setDataFormat(workbook.createDataFormat().getFormat(
						xlsStyle.getFormat()));
			}
			if (xlsStyle.isFillBackground()) {
				hssfStyle.setFillForegroundColor(xlsStyle.getBgcolor());
				hssfStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
			}

			XlsFont font = xlsStyle.getFont();
			HSSFFont hssfFont = workbook.createFont();
			hssfFont.setFontName(font.getFontName());
			hssfFont.setFontHeightInPoints(font.getHeight());
			hssfFont.setColor(font.getColor());
			if (font.isBold())
				hssfFont.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
			if (font.isUnderline())
				hssfFont.setUnderline(HSSFFont.U_SINGLE);
			hssfFont.setItalic(font.isItalic());
			hssfFont.setStrikeout(font.isStrikeout());
			hssfStyle.setFont(hssfFont);

			xlsStyle.setHssfStyle(hssfStyle);
		}
	}

	private void exportXlsFileRecursion(Object obj, Class clazz)
			throws IllegalContentException, IllegalStyleException {
		if (obj instanceof XlsUnit) {
			XlsUnit unit = (XlsUnit) obj;
			Object contents[] = unit.getXlsContents();
			if (contents == null) {
				return;
			}
			for (int i = 0; i < contents.length; i++) {
				/* Get content. */
				Object content = null;
				if (contents[i] == null) {
					if (clazz.equals(XlsTData.class))
						content = new XlsTData("");
					else
						continue;
				} else if (isLegalType(contents[i])) {
					if (contents[i] instanceof String
							&& clazz.equals(XlsTRow.class)) {
						content = new SimpleTRow((String) contents[i]);
					} else {
						content = new XlsTData(contents[i]);
					}
				} else if (contents[i] instanceof XlsUnit
						|| contents[i] instanceof Collection
						|| contents[i].getClass().isArray()) {
					content = contents[i];
				} else {
					throw new IllegalContentException(contents[i].getClass());
				}
				/* Deal with content. */
				if (clazz.equals(XlsTData.class) && content instanceof XlsTData) {
					/* XlsTData in XlsTRow. */
					createTData((XlsTData) content);
				} else if (clazz.equals(XlsTRow.class)
						&& content instanceof XlsTRow) {
					/* XlsTRow in XlsTable. */
					createTRow((XlsTRow) content);
				} else if (clazz.equals(XlsTRow.class)
						&& content instanceof XlsTData) {
					/* Single TRow contents only a String TData. */
					createTRow(new SimpleTRow((XlsTData) content));
				} else if (content instanceof Collection
						|| content.getClass().isArray()) {
					/* Iterate collection/array. */
					exportXlsFileRecursion(content, clazz);
				} else if (content instanceof XlsCombo) {
					/* Combo XlsUnit. */
					exportXlsFileRecursion(content, clazz);
				} else {
					/* Illegal content. */
					if (content instanceof XlsUnit || isLegalType(content)) {
						throw new IllegalContentException(content.getClass(),
								clazz);
					} else {
						throw new IllegalContentException(content.getClass());
					}
				}
			}
		} else if (obj instanceof Collection) {
			/* Collection. */
			int _y = y;
			short _x = x;
			for (Iterator it = ((Collection) obj).iterator(); it.hasNext();) {
				iterationBody(it.next(), clazz);
			}
			if (groupRows && clazz.equals(XlsTRow.class))
				sheet.groupRow(_y, y - 1);
			else if (groupCols && clazz.equals(XlsTData.class))
				sheet.groupColumn(_x, (short) (x - 1));
		} else if (obj.getClass().isArray()) {
			/* Array. */
			int _y = y;
			short _x = x;
			for (int i = 0; i < Array.getLength(obj); i++) {
				iterationBody(Array.get(obj, i), clazz);
			}
			if (groupRows && clazz.equals(XlsTRow.class))
				sheet.groupRow(_y, y - 1);
			else if (groupCols && clazz.equals(XlsTData.class))
				sheet.groupColumn(_x, (short) (x - 1));
		} else {
			/* Illegal content. */
			throw new IllegalContentException(obj.getClass());
		}
	}

	private void iterationBody(Object obj, Class clazz)
			throws IllegalContentException, IllegalStyleException {
		if (obj == null || clazz.isAssignableFrom(obj.getClass())) {
			/* Meet end condition. */
			if (clazz.equals(XlsTRow.class)) {
				createTRow((XlsTRow) obj);
			} else if (clazz.equals(XlsTData.class)) {
				createTData((XlsTData) obj);
			}
		} else if (clazz.equals(XlsTData.class) && isLegalType(obj)) {
			/*  */
			createTData(new XlsTData(obj));
		} else if (obj instanceof XlsCombo) {
			/* Combo XlsUnit. */
			exportXlsFileRecursion(obj, clazz);
		} else if (obj instanceof Collection || obj.getClass().isArray()) {
			/* Nested collection/array. */
			exportXlsFileRecursion(obj, clazz);
		} else {
			/* Illegal content. */
			if (obj instanceof XlsUnit || isLegalType(obj)) {
				throw new IllegalContentException(obj.getClass(), clazz);
			} else {
				throw new IllegalContentException(obj.getClass());
			}
		}
	}

	private void createTRow(XlsTRow row) throws IllegalContentException,
			IllegalStyleException {
		if (row == null) {
			return;
		}
		short temp = x;
		exportXlsFileRecursion(row, XlsTData.class);
		y++;
		x = temp;
	}

	private void createTData(XlsTData data) throws IllegalStyleException {
		if (data == null)
			data = new XlsTData("");

		/* Get cell. */
		if ((row = sheet.getRow(y)) == null)
			row = sheet.createRow(y);

		cell = getCell(x);

		/* Create cell style if not specified. */
		data.setStyle(xlsFile.getStyle(data.getStyleIndex()));

		Object value = data.getValue();
		if (value instanceof String) {
			if (!data.isFormula()) {
				HSSFRichTextString str = new HSSFRichTextString((String) value);
				cell.setCellValue(str);
				cell.setCellType(HSSFCell.CELL_TYPE_STRING);
			} else {
				cell.setCellFormula((String) value);
				cell.setCellType(HSSFCell.CELL_TYPE_FORMULA);
			}
		} else if (value instanceof BigDecimal) {
			cell.setCellValue(((BigDecimal) value).doubleValue());
			cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC);
		} else if (value instanceof Date) {
			cell.setCellValue((Date) value);
			cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC);
			if (data.getStyleIndex() == XlsFile.CUSTOM_STYLES_NUM) {
				data.setStyleIndex(XlsFile.CUSTOM_STYLES_NUM + 1);
			}
		} else if (value instanceof Boolean) {
			cell.setCellValue(((Boolean) value).booleanValue());
			cell.setCellType(HSSFCell.CELL_TYPE_BOOLEAN);
		}
		/*
		 * Merge cell. Create a new cell(with null value) for every other cell
		 * in the merged cell.
		 */
		if (data.getColspan() != 1 || data.getRowspan() != 1) {
			short _x = (short) (x + data.getColspan() - 1);
			int _y = y + data.getRowspan() - 1;
			int row = y;
			do {
				if (sheet.getRow(row) == null)
					sheet.createRow(row);
				short col = x;
				do {
					if (sheet.getRow(row).getCell(col) == null)
						sheet.getRow(row).createCell(col);
				} while (++col <= _x);
			} while (++row <= _y);
			sheet.addMergedRegion(new Region(y, x, _y, _x));
		}
		/* Set cell style. */
		if (xlsFile.getStyle(data.getStyleIndex()).getHssfStyle() == null) {
			throw new IllegalStyleException();
		} else {
			cell.setCellStyle(xlsFile.getStyle(data.getStyleIndex())
					.getHssfStyle());
		}
		/* Adjust row index. */
		x += data.getColspan();
	}

	/*
	 * If the cell in specified column is NOT null(it is in a merged cell), then
	 * choose next colunm.
	 */
	private HSSFCell getCell(short colNum) {
		return (row.getCell(x) == null) ? row.createCell(x) : getCell(++x);
	}

	/*
	 * wheather given object's type is legal or not.
	 */
	private boolean isLegalType(Object o) {
		return (o instanceof String || o instanceof BigDecimal
				|| o instanceof Date || o instanceof Boolean);
	}

	/**
	 * @param groupRows
	 *            wheather group rows(in a collection or array) or not when
	 *            exporting excel file.
	 */
	public void setGroupRows(boolean groupRows) {
		this.groupRows = groupRows;
	}

	/**
	 * @param groupCols
	 *            wheather group columns(in a collection or array) or not when
	 *            exporting excel file.
	 */
	public void setGroupCols(boolean groupCols) {
		this.groupCols = groupCols;
	}
}
